package ch.jamme.impl;

import java.io.*;
import java.lang.reflect.*;
import java.util.*;
import java.util.concurrent.*;

import org.jdom.*;
import org.jdom.input.*;
import org.jdom.output.*;

import ch.jamme.conf.*;
import ch.jamme.exception.*;
import ch.jamme.reference.*;
import ch.jamme.util.*;

/**
 * The engine that actually performs the transformation of an object to and from an xml string
 * This class is instantiated once for each transformation in order to guarantee its thread confinement
 * @author Mario Fusco
 */
public class MarshallerEngine {
	
	private MarshallerConfiguration configuration;
	
	MarshallerEngine(MarshallerConfiguration marshallerConfiguration) { 
		this.configuration = marshallerConfiguration;
	}
	
	MarshallerConfiguration getConfiguration() {
		return configuration;
	}
	
	// ////////////////////////////////////////////////////////////////////////
	// /// Marshall
	// ////////////////////////////////////////////////////////////////////////

	private int objectCounter = 1;
	
	private final Map<Object, Integer> parsedObjects = new HashMap<Object, Integer>();
	private final Map<Object, Object> parentsTree = new HashMap<Object, Object>();
	private Object root;
	
	String toXml(Object object) throws MarshallException {
		int parallelThreadsNumber = configuration.getMarshallerThreadStrategy().getMarshallerThreadsNumber(object);
		ObjectToXmlConvertor objectToXmlConvertor = parallelThreadsNumber > 1 ? 
				new ObjectToXmlProducer(this, parallelThreadsNumber) : new ObjectToXmlConvertor(this);
		
		Writer writer = new StringWriter();
		XMLOutputter outputter = new XMLOutputter(Format.getPrettyFormat());
		try {
			root = object;
			registerParsingObject(object, null);
			outputter.output(new Document(objectToXmlConvertor.toElement(object)), writer);
		} catch (MarshallException me) {
			throw me;
		} catch (Exception e) {
			throw new MarshallException(e);
		}	
		return writer.toString();
	}
	
	/**
	 * If this object has been already parsed it returns an element placeholder 
	 * containing a reference to the parsed item, otherwise it returns null
	 */
	Element getParsedElement(String elementName, Object object) {
		Integer objectRef = parsedObjects.get(object);
		if (objectRef == null) return null;
		Element refElement = new Element(elementName);
		if (configuration.getMarshallStyle().printObjectIds())
			refElement.setAttribute(configuration.getNamingConvention().getIdentifierReferenceAttribute(), objectRef.toString());
		return refElement;
	}
	
	Integer registerParsingObject(Object object, Object parent) {
		Integer objectId = objectCounter++;
		parsedObjects.put(object, objectId);
		if (configuration.getMarshallStyle().checkLoops()) parentsTree.put(object, parent);
		return objectId;
	}
	
	boolean isChildOf(Object child, Object ancestor) {
		Object parent = parentsTree.get(child);
		if (parent == null) return child.equals(root);
		if (parent.equals(ancestor)) return true;
		return isChildOf(parent, ancestor);
	}
	
	// ////////////////////////////////////////////////////////////////////////
	// /// Unmarshall
	// ////////////////////////////////////////////////////////////////////////
	
	private Map<String, Object> resolvedObjects;
	private List<ForwardReference> forwardReferences;
	
	<T> T toObject(String xml, Class<T> rootClass) throws UnmarshallException {
		int parallelThreadsNumber = configuration.getMarshallerThreadStrategy().getUnmarshallerThreadsNumber(xml);
		XmlToObjectConvertor xmlToObjectConvertor = null;
		if (parallelThreadsNumber > 1) {
			resolvedObjects = new ConcurrentHashMap<String, Object>(16, 0.75f, parallelThreadsNumber);
			forwardReferences = Collections.synchronizedList(new LinkedList<ForwardReference>());
			xmlToObjectConvertor = new XmlToObjectProducer(this, parallelThreadsNumber);	
		} else {
			resolvedObjects = new HashMap<String, Object>();
			forwardReferences = new LinkedList<ForwardReference>();
			xmlToObjectConvertor = new XmlToObjectConvertor(this);	
		}
		
		try {
			T result = xmlToObjectConvertor.convertXmlToObject(new SAXBuilder().build(new StringReader(xml)).getRootElement(), rootClass);
			
			// Resolves the references missing during the sequential parsing process
			Iterator<ForwardReference> referencesIterator = forwardReferences.iterator();
			while (referencesIterator.hasNext()) 
				if (referencesIterator.next().resolve(resolvedObjects)) referencesIterator.remove();

			// Checks that there are no unresolved references
			if (!configuration.isFailSafe() && !forwardReferences.isEmpty()) throw new UnresolvedReferenceException(forwardReferences.get(0));
			return result;
		} catch (UnmarshallException ue) {
			throw ue;
		} catch (Exception e) {
			throw new UnmarshallException(e);
		}
	}
	
	Object getResolvedObject(String idRef, Field field) {
		Object result = resolvedObjects.get(idRef);
		if (result != null) return result;
		Logger.debug(this, "Unable to resolve referenced object " + idRef);
		ForwardReference ref = new ForwardReference(idRef, field);
		forwardReferences.add(ref);
		return ref;
	}
	
	void registerResolveObject(String id, Object object) {
		resolvedObjects.put(id, object);
	}
}
